package ru.simplemaps.osmapi.common;

import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

/**
 * Bounding box for longitude, lattitude
 * WARNING: bottom is less than top. calculated from the bottom to top
 *
 * @author enaku_adm
 * @since 23.07.2010 9:51:24
 */
public final class BBox {
    /**
     * minimal longitude
     */
    public final double left;

    /**
     * minimal lattitude
     */
    public final double bottom;

    /**
     * maximal longitude
     */
    public final double right;

    /**
     * maximal lattitude
     */
    public final double top;

    /**
     * Creates bounding box instance
     *
     * @param left   minimal longitude
     * @param bottom minimal lattitude
     * @param right  maximal longitude
     * @param top    maximal lattitude
     */
    public BBox(double left, double bottom, double right, double top) {
        this.bottom = bottom;
        this.left = left;
        this.right = right;
        this.top = top;
    }

    private static boolean between(double check, double from, double to) {
        return check > from && check < to;
    }

    /**
     * Subtracts subtracted box from this box
     * todo сравнить этот алгоритм с другим: можно просто сделать два сортедсета и потом пробежать по ним. думаю, что этот будет быстрее
     *
     * @param subtracted box to subtract
     * @return result of subtraction
     */
    public Collection<BBox> minus(BBox subtracted) {
        double[] netX = new double[4];
        double[] netY = new double[4];

        int xLength = 0;
        int yLength = 0;

        BBox i = intersection(subtracted);
        if (i == null) return Arrays.asList(this); //todo optimize. create some simple collection

        netX[xLength] = left;
        xLength++;

        if (i.left != left) {
            netX[xLength] = i.left;
            xLength++;
        }

        if (i.right != right) {
            netX[xLength] = i.right;
            xLength++;
        }

        netX[xLength] = right;
        xLength++;

        netY[yLength] = bottom;
        yLength++;

        if (bottom != i.bottom) {
            netY[yLength] = i.bottom;
            yLength++;
        }

        if (top != i.top) {
            netY[yLength] = i.top;
            yLength++;
        }

        netY[yLength] = top;
        yLength++;

        List<BBox> retVal = new LinkedList<BBox>();

        for (int j = 0; j < xLength - 1; j++) {
            for (int k = 0; k < yLength - 1; k++) {
                BBox box = new BBox(netX[j], netY[k], netX[j + 1], netY[k + 1]);

                if (!box.equals(i))
                    retVal.add(box);
            }
        }

        return retVal;
    }

    /**
     * Calculates intersection box between this box and other box
     *
     * @param other other box
     * @return intersection box (null if boxes do not intersect)
     */
    public BBox intersection(BBox other) {
        double newLeft = max(left, other.left);
        double newRight = min(right, other.right);
        double newTop = min(top, other.top);
        double newBottom = max(bottom, other.bottom);

        if (newLeft < newRight && newBottom < newTop) {
            return new BBox(newLeft, newBottom, newRight, newTop);
        }

        return null;
    }

    private static double max(double a, double b) {
        return (a >= b) ? a : b;
    }

    private static double min(double a, double b) {
        return (a <= b) ? a : b;
    }

//    /**
//     * Subtracts box from the list of boxes (from every box)
//     *
//     * @param retVal     list of boxes to subtract from
//     * @param subtracted box to subtruct
//     */
//    private static void minus(Collection<BBox> retVal, BBox subtracted) {
//        List<BBox> toRemove = new LinkedList<BBox>();
//        for (BBox bBox : retVal) {
//            Collection<BBox> boxes = bBox.minus(subtracted);
//            if (boxes.size() != 1) {
//                retVal.addAll(boxes);
//                toRemove.add(bBox);
//            }
//        }
//
//        for (BBox bBox : toRemove) {
//            retVal.remove(bBox);
//        }
//    }
//

    /**
     * Subtracts boxes from this box
     *
     * @param boxes boxes to minus
     * @return result of minus operation
     */
    public Collection<BBox> minus(Collection<BBox> boxes) {
        Collection<BBox> currentList = new LinkedList<BBox>();
        currentList.add(this);

        for (BBox toSubtract : boxes) {
            currentList = subtract(currentList, toSubtract);
        }

        return currentList;
    }

    /**
     * Subtracts one box from many boxes
     *
     * @param boxes    boxes to subtract from
     * @param subtract box to subtract
     * @return collection of boxes
     */
    private static Collection<BBox> subtract(Collection<BBox> boxes, BBox subtract) {
        LinkedList<BBox> retVal = new LinkedList<BBox>();

        for (BBox box : boxes) {
            retVal.addAll(box.minus(subtract));
        }

        return retVal;
    }

    /**
     * Разбить область на атомарные области исходя из размеров сетки
     *
     * @param netSize размер сетки
     * @return набор полученных атомарных областей
     */
    public Collection<BBox> simplify(double netSize) {
        double leftNetSize = left / netSize;
        int fullLeftNetSize = (int) Math.floor(leftNetSize);

        double rightNetSize = right / netSize;
        int fullRightNetSize = (int) (Math.floor(rightNetSize) + 1);

        double bottomNetSize = bottom / netSize;
        int fullBottomNetSize = (int) Math.floor(bottomNetSize);

        double topNetSize = top / netSize;
        int fullTopNetSize = (int) (Math.floor(topNetSize) + 1);

        List<BBox> result = new LinkedList<BBox>();

        for (int netX = fullLeftNetSize; netX < fullRightNetSize; netX++) {
            for (int netY = fullBottomNetSize; netY < fullTopNetSize; netY++) {
                result.add(
                        new BBox(
                                netX * netSize,
                                netY * netSize,
                                (netX + 1) * netSize,
                                (netY + 1) * netSize
                        )
                );
            }
        }

        return result;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        BBox bBox = (BBox) o;

        if (Double.compare(bBox.bottom, bottom) != 0) return false;
        if (Double.compare(bBox.left, left) != 0) return false;
        if (Double.compare(bBox.right, right) != 0) return false;
        if (Double.compare(bBox.top, top) != 0) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result;
        long temp;
        temp = left != +0.0d ? Double.doubleToLongBits(left) : 0L;
        result = (int) (temp ^ (temp >>> 32));
        temp = bottom != +0.0d ? Double.doubleToLongBits(bottom) : 0L;
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        temp = right != +0.0d ? Double.doubleToLongBits(right) : 0L;
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        temp = top != +0.0d ? Double.doubleToLongBits(top) : 0L;
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        return result;
    }

    @Override
    public String toString() {
        return "[" + left + "," + bottom + ":" + right + "," + top + "]";
    }
}
